﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Data.SqlClient;
using System.Configuration;
using System.Data;
using System.Web.Caching;
using Medianamik.Core.Interfaces;
using Medianamik.Core.Logging;

namespace Medianamik.Core.CodeGeneration
{
    public class CodeService
    {
        private static string ConnectionString
        {
            get
            {
                return ConfigurationManager.ConnectionStrings["CS_dbo"].ConnectionString;
            }
        }

        private static ILogger _logger;
        private static ILogger Logger
        {
            get
            {
                return _logger
                    ?? (_logger = new ObjectFactory<ILogger>().Get("CodeServiceLogger"));
            }
        }

        public void DoTheMagic(NodeType type)
        {
            GenerateTableAndView(type);
            FillTable(type);
        }

        public CodeGenerationResult GenerateClientCode(string absoluteOutputPath)
        {
            var startTime = DateTime.Now;
            var finalCode = new CodeGenerator().RunAndWriteFiles(absoluteOutputPath);
            var finalResult = new CodeGenerationResult(finalCode, null, DateTime.Now - startTime);

            return finalResult;
        }

        public CodeGenerationResult GenerateMyProfileClass(string absoluteOutputPath)
        {
            var startTime = DateTime.Now;
            var finalCode = new CodeGenerator().GenerateMyProfileClass(absoluteOutputPath);
            var finalResult = new CodeGenerationResult(finalCode, null, DateTime.Now - startTime);

            return finalResult;
        }

        public void GenerateTableAndView(NodeType pNodeType)
        {
            ExecuteDbAction(conn =>
            {
                var builder = new CommandBuilder(conn);
                var commands = new List<IDbCommand>
                   {
                       builder.BuildDropViewCommand(pNodeType),
                       builder.BuildDropTableCommand(pNodeType),
                       builder.BuildCreateTableCommand(pNodeType),
                       builder.BuildCreateViewCommand(pNodeType)
                   };

                commands.ForEach(c => ExecuteCommand(c));

                try
                {
                    var tableName = new SqlNameGenerator(pNodeType).TableNameSimple;
                    SqlCacheDependencyAdmin.EnableTableForNotifications(ConnectionString, tableName);
                }
                catch (Exception)
                {
                    Logger.Error("Le type " + pNodeType.Name + " n'a pas pu être regénéré.");
                }
            });
        }

        public void RegenerateUserProfileView()
        {
            ExecuteDbAction(conn =>
                                {
                                    var commands = new List<IDbCommand>() { };
                                    const string dropUserProfileViewNonQuery =
                                        @"IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[vw_UserProfile]')) BEGIN drop view [dbo].[vw_UserProfile] END";
                                    var command = new SqlCommand(dropUserProfileViewNonQuery, conn);
                                    commands.Add(command);

                                    const string createUserProfileViewNonQuery =
                                        @"CREATE VIEW [dbo].[vw_UserProfile]
                                        AS
                                        SELECT p.*, u.[UserId], u.[Name], u.[LastLockedOutDate], u.[IsLockedOut], u.[PasswordQuestion], u.[PasswordAnswer], u.[Comment], u.[LastPasswordChangedDate], u.[IsApproved], u.[LastActivityDate], u.[Email], u.[Password], u.[Salt], u.[PasswordFormat]
                                        FROM dbo.vw_gen_MedianamikProfile AS p INNER JOIN
                                            dbo.medianamik_User AS u ON p.[User] = u.UserId AND CultureName = ''";

                                    command = new SqlCommand(createUserProfileViewNonQuery, conn);
                                    commands.Add(command);
                                    commands.ForEach(c => ExecuteCommand(c));
                                }
                );
        }

        public void GenerateView(NodeType pNodeType)
        {
            ExecuteDbAction(conn =>
            {
                var builder = new CommandBuilder(conn);
                var commands = new List<IDbCommand>
                   {
                       builder.BuildDropViewCommand(pNodeType),
                       builder.BuildCreateViewCommand(pNodeType)
                   };

                commands.ForEach(c => ExecuteCommand(c));
            });
        }

        public void GenerateView(NodeTypeInterface pInterface)
        {
            ExecuteDbAction(conn =>
            {
                var builder = new CommandBuilder(conn);
                var commands = new List<IDbCommand> { builder.BuildDropViewCommand(pInterface) };
                var implementers = NodeTypeManager.GetNodeTypes(pInterface.ID);

                if (implementers.Count() > 0)
                {
                    commands.Add(builder.BuildCreateViewCommand(pInterface, implementers
                        .Select(imp => imp.Owner)));
                }

                commands.ForEach(c => ExecuteCommand(c));
            });
        }

        public void AddColumns(IEnumerable<NodeTypeProperty> pNodeTypeProperties)
        {
            var type = pNodeTypeProperties.First().NodeType;
            var childTypes = NodeTypeManager.GetAllChildTypes(type.ID)
                           .Where(p => p.IsGenerated)
                           .OrderByDescending(t => t.Depth).ToList();
            childTypes.Add(type);

            ExecuteDbAction(conn =>
            {
                var builder = new CommandBuilder(conn);
                var commands = childTypes.SelectMany(t =>
                        builder.BuildAddColumnsCommand(pNodeTypeProperties, t))
                    .Concat(childTypes.Select(t => builder.BuildDropViewCommand(t)))
                    .Concat(childTypes.Select(t => builder.BuildCreateViewCommand(t)))
                    .ToList();

                commands.ForEach(c => ExecuteCommand(c));
            });
        }

        public void RemoveColumns(IEnumerable<NodeTypeProperty> pNodeTypeProperties)
        {
            var type = pNodeTypeProperties.First().NodeType;
            var childTypes = NodeTypeManager.GetAllChildTypes(type.ID)
                           .Where(p => p.IsGenerated)
                           .OrderByDescending(t => t.Depth).ToList();
            childTypes.Add(type);

            ExecuteDbAction(conn =>
            {
                var builder = new CommandBuilder(conn);
                var commands = childTypes.SelectMany(t =>
                        builder.BuildRemoveColumnsCommand(pNodeTypeProperties, t))
                    .Concat(childTypes.Select(t => builder.BuildDropViewCommand(t)))
                    .Concat(childTypes.Select(t => builder.BuildCreateViewCommand(t)))
                    .ToList();

                commands.ForEach(c => ExecuteCommand(c));
            });
        }

        public void UpdateViewForParents(NodeType pNodeType)
        {
            var parentTypes = NodeTypeManager.GetParentTypes(pNodeType.ID, false)
               .Where(p => p.IsGenerated)
               .OrderByDescending(t => t.Depth);

            ExecuteDbAction(conn =>
               {
                   var builder = new CommandBuilder(conn);
                   var commands = parentTypes.Select(t => builder.BuildDropViewCommand(t))
                       .Concat(parentTypes.Select(t => builder.BuildCreateViewCommand(t)))
                       .ToList();

                   commands.ForEach(c => ExecuteCommand(c));
               });
        }

        public void GenerateAllTablesAndViews()
        {
            var allTypes = NodeTypeManager.GetAllTypes(true)
                .Where(t => t.IsGenerated)
                .OrderByDescending(t => t.Depth);

            ExecuteDbAction(conn =>
             {
                 var builder = new CommandBuilder(conn);
                 var commands = allTypes.Select(t => builder.BuildDropViewCommand(t))
                     .Concat(allTypes.Select(t => builder.BuildDropTableCommand(t)))
                     .Concat(allTypes.Select(t => builder.BuildCreateTableCommand(t)))
                     .Concat(allTypes.Select(t => builder.BuildCreateViewCommand(t)))
                     .ToList();

                 commands.ForEach(c => ExecuteCommand(c));
                 var tableNames = allTypes.Select(t => new SqlNameGenerator(t).TableNameSimple).ToArray();
                 SqlCacheDependencyAdmin.EnableTableForNotifications(ConnectionString, tableNames);
             });
        }

        public void DeleteTableAndView(NodeType pNodeType)
        {
            var parentTypes = Enumerable.Empty<NodeType>();

            if (pNodeType.ParentID.HasValue)
            {
                parentTypes = NodeTypeManager.GetParentTypes(pNodeType.ParentID.Value, true)
                    .Where(t => t.IsGenerated).ToList();
            }

            ExecuteDbAction(conn =>
            {
                var builder = new CommandBuilder(conn);
                ExecuteCommand(builder.BuildDropTableCommand(pNodeType));
                ExecuteCommand(builder.BuildDropViewCommand(pNodeType));
                var commands = parentTypes.Select(t => builder.BuildDropViewCommand(t))
                    .Concat(parentTypes.Select(t => builder.BuildCreateViewCommand(t))).ToArray();

                commands.ForEach(c => ExecuteCommand(c));
            });

        }

        public bool CheckTableExists(NodeType pNodeType)
        {
            var tableExists = false;

            ExecuteDbAction(conn =>
            {
                var command = new CommandBuilder(conn).BuildTableExistsCommand(pNodeType);

                using (var reader = command.ExecuteReader())
                {
                    if (reader.Read()) { tableExists = true; }
                    reader.Close();
                }
            });

            return tableExists;
        }

        public void InsertRecord(NodeInstance pNodeInstance, bool publication)
        {
            ExecuteDbAction(conn => ExecuteCommand(new CommandBuilder(conn)
                .BuildInsertCommand(pNodeInstance, publication)));
        }

        public void InsertAllRecords(NodeInstance pNodeInstance)
        {
            ExecuteDbAction(conn => CreateAllRecordsForNodeInstance(pNodeInstance, conn));
        }

        public void InsertDraftRecord(NodeInstance pNodeInstance)
        {
            ExecuteDbAction(conn => ExecuteCommand(new CommandBuilder(conn)
                .BuildInsertCommand(pNodeInstance, false)));
        }

        public void DeleteDraftAndPublished(NodeInstance pNodeInstance)
        {
            ExecuteDbAction(conn => ExecuteCommand(new CommandBuilder(conn)
                .BuildDeleteDraftAndPublishedCommand(pNodeInstance)));
        }

        public void DeleteDraftRecord(NodeInstance pNodeInstance)
        {
            ExecuteDbAction(conn => ExecuteCommand(new CommandBuilder(conn)
                .BuildDeleteDraftRecordCommand(pNodeInstance)));
        }

        public void DeletePublishedRecord(NodeInstance pNodeInstance)
        {
            ExecuteDbAction(conn => ExecuteCommand(new CommandBuilder(conn)
                .BuildDeletePublishedRecordCommand(pNodeInstance)));
        }

        public void UpdateRecord(NodeInstance nodeInstance)
        {
            ExecuteDbAction(conn => ExecuteCommand(new CommandBuilder(conn)
                .BuildUpdateCommand(nodeInstance)));
        }

        public void FillTable(NodeType pNodeType)
        {
            DisableTableTriggers(pNodeType);

            var i = 0;
            var nodes = NodeManager.GetNodesByType(pNodeType.ID, true, i, 100);

            while (nodes.Any())
            {
                Logger.Warn("Régénération du type " + pNodeType.Name + " batch " + (i + 1));
                i++;
                var localNodes = nodes;
                ExecuteDbAction(conn => localNodes.ForEach(n => CreateRecords(n, conn)));
                nodes = NodeManager.GetNodesByType(pNodeType.ID, true, i, 100);
            }

            EnableTableTriggers(pNodeType);
        }

        public void NotifyTableChanged(NodeType pNodeType)
        {
            ExecuteDbAction(conn =>
                ExecuteCommand(new CommandBuilder(conn).BuildNotifyTableChangedCommand(pNodeType)));
        }

        public void DisableTableTriggers(NodeType pNodeType)
        {
            ExecuteDbAction(conn =>
                ExecuteCommand(new CommandBuilder(conn).BuildDisableTableTriggers(pNodeType)));
        }

        public void DisableTableConstraints(string tableName)
        {
            ExecuteDbAction(conn =>
                ExecuteCommand(new CommandBuilder(conn).BuildDisableTableConstraints(tableName)));
        }

        public void EnableTableConstraints(string tableName)
        {
            ExecuteDbAction(conn =>
                ExecuteCommand(new CommandBuilder(conn).BuildEnableTableConstraints(tableName)));
        }

        public void EnableTableConstraint(string tableName, string constraintName)
        {
            ExecuteDbAction(conn => ExecuteCommand(new CommandBuilder(conn)
                .BuildEnableTableConstraint(tableName, constraintName)));
        }

        public void DisableTableConstraint(string tableName, string constraintName)
        {
            ExecuteDbAction(conn => ExecuteCommand(new CommandBuilder(conn)
                .BuildDisableTableConstraint(tableName, constraintName)));
        }

        public void EnableTableTriggers(NodeType pNodeType)
        {
            ExecuteDbAction(conn => ExecuteCommand(new CommandBuilder(conn)
                .BuildEnableTableTriggers(pNodeType)));
        }

        public void SwitchToNonVersionableTypeMaintenance(NodeType pNodeType)
        {
            ExecuteDbAction(conn => ExecuteCommand(new CommandBuilder(conn)
                .BuildSwitchToNonVersionableTypeMaintenanceCommand(pNodeType)));
        }

        public void CleanUpDatabase()
        {
            ExecuteDbAction(conn =>
            {
                var builder = new CommandBuilder(conn);
                var command1 = builder.BuildDestroyDeletedNodesCommand();
                command1.CommandTimeout = 1800;
                var command2 = builder.BuildCleanUpGenTablesCommand();
                command2.CommandTimeout = 1800;
                var command3 = builder.BuildCleanUpRevisionsCommand(false);
                command3.CommandTimeout = 1800;
                ExecuteCommand(command1, true);
                ExecuteCommand(command2, true);
                ExecuteCommand(command3, true);
            });
        }

        public void CleanUpNodeRevisions()
        {
            ExecuteDbAction(conn =>
            {
                var builder = new CommandBuilder(conn);
                var command3 = builder.BuildCleanUpRevisionsCommand(false);
                command3.CommandTimeout = 1800;
                ExecuteCommand(command3, true);
            });
        }

        public void RemoveNeutralLangInstancesForNonNeutralLangNode(IEnumerable<NodeType> nonNeutralLangTypes)
        {
            if (nonNeutralLangTypes.Any(t => t.IsLanguageNeutral))
                throw new Exception("All types passed to RemoveNeutralLangInstancesForNonNeutralLangNode should not be language neutral.");

            ExecuteDbAction(conn =>
            {
                var command = new CommandBuilder(conn)
                    .BuildRemoveNeutralLangInstancesForNonNeutralLangNodeCommand(nonNeutralLangTypes);
                command.CommandTimeout = 180;

                ExecuteCommand(command, true);
            });
        }

        public void RemoveNonNeutralLangInstancesForNeutralLangNode(IEnumerable<NodeType> neutralLangTypes)
        {
            if (neutralLangTypes.Any(t => !t.IsLanguageNeutral))
                throw new Exception("All types passed to RemoveNonNeutralLangInstancesForNeutralLangNode should be language neutral.");

            ExecuteDbAction(conn =>
            {
                var command = new CommandBuilder(conn)
                    .BuildRemoveNonNeutralLangInstancesForNeutralLangNodeCommand(neutralLangTypes);
                command.CommandTimeout = 180;

                ExecuteCommand(command, true);
            });
        }

        private void CreateRecords(INode pNode, SqlConnection pConn)
        {
            pNode.Instances.ForEach(ni => CreateAllRecordsForNodeInstance(ni, pConn));
        }

        private void CreateAllRecordsForNodeInstance(NodeInstance pNodeInstance, SqlConnection pConn)
        {
            var builder = new CommandBuilder(pConn);
            var commands = new[] { builder.BuildInsertCommand(pNodeInstance, false) }.ToList();

            if (pNodeInstance.Node.NodeType.IsVersioned)
                commands.Add(builder.BuildInsertCommand(pNodeInstance, true));

            commands.ForEach(c => ExecuteCommand(c));
        }

        private void ExecuteDbAction(Action<SqlConnection> pAction)
        {
            using (var conn = new SqlConnection(ConnectionString))
            {
                conn.Open();
                pAction(conn);
                conn.Close();
            }
        }

        private int ExecuteCommand(IDbCommand pCommand)
        {
            return ExecuteCommand(pCommand, false);
        }
        private int ExecuteCommand(IDbCommand pCommand, bool pThrowOnError)
        {
            if (pCommand == null)
            {
                throw new ArgumentNullException("pCommand");
            }

            var affectedRows = 0;

            try
            {
                affectedRows = pCommand.ExecuteNonQuery();
            }
            catch (Exception e)
            {
                Logger.Error(e.Message + " : " + pCommand.CommandText, e);
                if (pThrowOnError) throw;
            }

            return affectedRows;
        }
    }
}
